5.3 select的底层实现及工作原理

select语句是Go语言中用于处理多个通道操作的一个强大工具,它能够在多个通道上同时进行非阻塞的选择操作。这对于实现并发程序的灵活性和复杂性处理非常有帮助。

本节我们将详细探讨select的内部实现及工作原理。

本节代码存放目录为 lesson15

select的底层实现

select的基本结构

select语句的实现涉及到以下几个核心部分:

  • 通道的case列表select语句中每个case会对应一个通道操作,编译器会将这些case打包成一个select操作列表。

    case结构如下所示:

      type scase struct {
          c    *hchan         // chan
          elem unsafe.Pointer // data element
      }
    
  • 随机化:为了避免select语句的饥饿问题(总是先处理某个case),Go语言的实现会对case列表进行随机化处理。

  • 阻塞队列:如果所有的通道都无法立即进行操作,select语句会将当前的Goroutine加入到每个通道的等待队列中,并阻塞Goroutine,直到某个通道的操作可以进行。

  • 唤醒与继续:当某个通道的操作可以进行时,select会唤醒相关的Goroutine,并继续执行与该通道关联的case


select的操作流程

我们可以以下步骤理解select的操作流程:

  1. 初始化selectcase列表:编译器将每个case操作(通道的接收或发送)打包到一个列表中。

  2. 随机化case列表:为了避免饥饿,运行时会对这个case列表进行随机打乱,使得每次select的执行顺序都是随机的。

  3. 遍历case列表

    • 对于每个caseselect语句会检查对应通道是否可以立即进行操作。

    • 如果可以,则直接执行该case,并结束select语句。

    • 如果不可以,则将当前Goroutine加入到该通道的等待队列中。

  4. 阻塞当前Goroutine

    • 如果所有的通道都不能立即操作,select语句将阻塞当前的Goroutine,直到其中一个通道可以进行操作。

    • 当某个通道准备好后,该Goroutine会被唤醒,执行与该通道关联的case

  5. 默认情况default

    • 如果select语句中存在default分支,并且所有通道都不能操作,那么select会立即执行default分支,而不会阻塞。

我们可以通过下面的示意图来进行理解:

┌────────────────────────┐
│       select           │
│  ┌───────────────────┐ │
│  │ case1: <- ch1     │ │
│  │ case2: <- ch2     │ │
│  │ case3: <- ch3     │ │
│  └───────────────────┘ │
└────────────────────────┘
          │
          ▼
┌─────────────────────────┐
│     运行时随机化          │
│  随机打乱 case 列表       │
└─────────────────────────┘
          │
          ▼
┌─────────────────────────┐
│  顺序检查 case           │
│  检查 case1case2...    │
│  按随机后的顺序           │
└─────────────────────────┘
          │
          ▼
┌─────────────────────────┐
│  执行一个可以操作的 case   │
│  例如:执行 case2         │
└─────────────────────────┘
          │
          ▼
  select 语句结束

select的实现原理

Go语言中的select语句依赖于调度器和通道的底层机制来实现。具体来说:

  • 调度器select语句会与Go调度器紧密合作,当select阻塞时,调度器会将当前Goroutine挂起,并将其加入到通道的等待队列中。

  • 通道的队列:每个通道都有发送和接收的等待队列。当select中的某个通道准备好时,通道的机制会从队列中唤醒对应的Goroutine

  • 唤醒机制:当通道的状态发生变化时(例如一个通道的数据被接收或发送),通道会通过调度器唤醒阻塞在其上的Goroutine,然后继续执行select语句的逻辑。

性能与使用建议

虽然select非常强大,但是在使用时也有一些性能和设计方面的考虑:

  1. 避免滥用select:在高并发场景下,如果select语句处理的通道数量过多,可能会带来一些性能开销。

  2. 使用default分支:在某些情况下,添加default分支可以防止select语句永久阻塞,从而提高程序的响应性。

  3. 关注select的随机性:由于select语句的case选择是随机化的,因此不要依赖某个固定的选择顺序,这样可以避免一些难以调试的问题。

下面代码演示了一个常用的使用案例:

func main() {
    ch1 := make(chan int64, 2)
    ch2 := make(chan int64, 2)
    ch3 := make(chan int64, 2)

    wg.Add(1)
    go func() {
        defer wg.Done()
        for {
            ch1 <- time.Now().Unix()
            time.Sleep(time.Duration(1) * time.Second)

            ch2 <- time.Now().Unix()
            time.Sleep(time.Duration(1) * time.Second)

            ch3 <- time.Now().Unix()
            time.Sleep(time.Duration(1) * time.Second)
        }
    }()

    wg.Add(1)
    go func() {
        defer wg.Done()
        for {
            select {
            case t1 := <-ch1:
                fmt.Println("Received from ch1, ", t1)
            case t2 := <-ch2:
                fmt.Println("Received from ch2, ", t2)
            case t3 := <-ch3:
                fmt.Println("Received from ch3, ", t3)
            }
        }
    }()
    wg.Wait()
}

小结

select的主要作用就是用于对多个通道执行读取操作,这样一方面我们可以简化我们的程序,一方面我们也可以通过select执行一些流程操作。

select本质上就属于是监听了多个通道,所以我们不适合在select中使用大批量的case

results matching ""

    No results matching ""